package com.stu.controller;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.stu.bean.CategoryEntity;
import com.stu.bean.SysLog;
import com.stu.service.DemoService;
import com.stu.service.RedisService;
import com.stu.vo.Catelog2Vo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author study
 * @version 1.0
 * @date 2021/6/9 15:07
 */

/*************************************************
 * Cacheable 触发保存到缓存
 * CacheEvict 触发从缓存中删除
 * CachePut 不影响方法执行更新缓存
 * Caching 组合以上多个操作
 * CacheConfig 类级别共享缓存相同配置
 * *************************************************
 * 使用
 * 1 导入依赖 spring-boot-starter-cache
 * 2 开启缓存 EnableCaching
 *
 * *************************************************
 * 缓存自动配置原理
 * CacheAutoConfiguration->RedisCacheConfiguration
 * *************************************************
 * 默认使用JDK序列化
 * 默认永不过期
 * -------------
 * 自定义
 * 指定生成的 key|支持Spel语法 https://docs.spring.io/spring-framework/docs/5.1.12.RELEASE/spring-framework-reference/integration.html#cache-spel-context
 * 指定生成的 缓存时间 spring.cache.redis.time-to-live
 **************************************************
 * spring cache的不足
 * 1 读模式
 *  缓存穿透，查询一个null数据。解决：spring.cache.redis.cache-null-values=true
 *
 *
 */

@Slf4j
@RestController
@RequestMapping("/redis")
@Api(tags = "Redis操作")
public class RedisController {
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    RedisService redisService;

    @ApiOperation(value = "缓存-map测试", notes = "缓存-map测试-springcache")
    @GetMapping({"/map"})
    public Map map() {
        return redisService.map();
    }

    @SneakyThrows
    @ApiOperation(value = "三级分类-cache", notes = "springcache测试")
    @GetMapping({"/cache"})
    public Map<String, List<Catelog2Vo>> catalogcache() {
        stringRedisTemplate.opsForValue().set("sys:user", RandomStringUtils.randomAlphabetic(5));
        return redisService.getDataFromDb();
    }

    @ApiOperation(value = "字符串-存储", notes = "redis-set操作")
    @GetMapping({"/set"})
    public List<SysLog> set() {
        stringRedisTemplate.opsForValue().set("sys:user", RandomStringUtils.randomAlphabetic(5));
        final String s = stringRedisTemplate.opsForValue().get("sys:user");
        log.info("RedisController.followList");
        System.out.println(getCatalogJson2());
        return Arrays.asList(new SysLog(RandomStringUtils.randomAlphabetic(5)));
    }

    @ApiOperation(value = "三级分类", notes = "redis操作：测试产生堆外内存溢出OutOfDirectMemoryError 异常")
    @GetMapping({"/catalog"})
    public Map<String, List<Catelog2Vo>> catalog() {
        stringRedisTemplate.opsForValue().set("sys:user", RandomStringUtils.randomAlphabetic(5));
        return getCatalogJson2();
    }

    //TODO 产生堆外内存溢出 OutOfDirectMemoryError:
    // io.netty.util.internal.OutOfDirectMemoryError
    //1)、springboot2.0以后默认使用lettuce操作redis的客户端，它使用通信
    //2)、lettuce的bug导致netty堆外内存溢出   可设置：-Dio.netty.maxDirectMemory
    //解决方案：不能直接使用-Dio.netty.maxDirectMemory去调大堆外内存
    //1)、升级lettuce客户端。      2）、切换使用jedis
    // @Override
    @Cacheable(value = {"catelog"}, key = "#root.targetClass")
    public Map<String, List<Catelog2Vo>> getCatalogJson2() {
        //给缓存中放json字符串，拿出的json字符串，反序列为能用的对象
        /**
         * 1、空结果缓存：解决缓存穿透问题
         * 2、设置过期时间(加随机值)：解决缓存雪崩
         * 3、加锁：解决缓存击穿问题
         */
        //1、加入缓存逻辑,缓存中存的数据是json字符串
        //JSON跨语言。跨平台兼容。
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        String catalogJson = ops.get("catalogJson");
        if (StringUtils.isEmpty(catalogJson)) {
            System.out.println("缓存不命中...查询数据库...");
            //2、缓存中没有数据，查询数据库
            Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbWithRedissonLock();
            return catalogJsonFromDb;
        }
        //转为指定的对象
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
        return result;
    }

    /**
     * 缓存里的数据如何和数据库的数据保持一致？？
     * 缓存数据一致性
     * 1)、双写模式
     * 2)、失效模式
     *
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
        //1、占分布式锁。去redis占坑
        //（锁的粒度，越细越快:具体缓存的是某个数据，11号商品） product-11-lock
        //RLock catalogJsonLock = redissonClient.getLock("catalogJson-lock");
        //创建读锁
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("catalogJson-lock");
        RLock rLock = readWriteLock.readLock();
        Map<String, List<Catelog2Vo>> dataFromDb = null;
        try {
            rLock.lock();
            //加锁成功...执行业务
            try {
                dataFromDb = getDataFromDb();
            } catch (IOException e) {
                log.error("读取文件失败");
            }
        } finally {
            rLock.unlock();
        }
        return dataFromDb;
    }

    private Map<String, List<Catelog2Vo>> getDataFromDb() throws IOException {
        //得到锁以后，我们应该再去缓存中确定一次，如果没有才需要继续查询
        String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
        if (!StringUtils.isEmpty(catalogJson)) {
            //缓存不为空直接返回
            Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });

            return result;
        }

        System.out.println("查询了数据库");

        /**
         * 将数据库的多次查询变为一次
         */
        List<CategoryEntity> selectList = this.selectList();

        //1、查出所有分类
        //1、1）查出所有一级分类
        List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);

        //封装数据
        Map<String, List<Catelog2Vo>> parentCid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //1、每一个的一级分类,查到这个一级分类的二级分类
            List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());

            //2、封装上面的结果
            List<Catelog2Vo> catelog2Vos = null;
            if (categoryEntities != null) {
                catelog2Vos = categoryEntities.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());

                    //1、找当前二级分类的三级分类封装成vo
                    List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId());

                    if (level3Catelog != null) {
                        List<Catelog2Vo.Category3Vo> category3Vos = level3Catelog.stream().map(l3 -> {
                            //2、封装成指定格式
                            Catelog2Vo.Category3Vo category3Vo = new Catelog2Vo.Category3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());

                            return category3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(category3Vos);
                    }

                    return catelog2Vo;
                }).collect(Collectors.toList());
            }

            return catelog2Vos;
        }));

        //3、将查到的数据放入缓存,将对象转为json
        String valueJson = JSON.toJSONString(parentCid);
        stringRedisTemplate.opsForValue().set("catalogJson", valueJson, 1, TimeUnit.DAYS);

        return parentCid;
    }

    /**
     * 从文本中 读取 模拟 读取数据库
     *
     * @return 集合
     * @throws IOException 文件找不到
     */
    private List<CategoryEntity> selectList() throws IOException {
        final String s = FileUtils.readFileToString(ResourceUtils.getFile("classpath:test.json"),
                StandardCharsets.UTF_8);
        final List<CategoryEntity> list = JSON.parseArray(s, CategoryEntity.class);
        return list;
    }

    private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList, Long parentCid) {
        List<CategoryEntity> categoryEntities = selectList.stream().filter(item -> item.getParentCid().equals(parentCid)).collect(Collectors.toList());
        return categoryEntities;
    }

    @Autowired
    DemoService demoService;

    @GetMapping({"/cacheTest"})
    @ApiOperation(value = "缓存key", notes = "redis-set操作")
    public List cacheTest() {
        return demoService.cacheKey();
    }
}
